package com.leyou.search.service.impl;


import com.leyou.common.dto.*;
import com.leyou.common.exception.LyException;
import com.leyou.item.client.ItemClient;
import com.leyou.item.dto.CategoryDTO;
import com.leyou.search.entity.Goods;
import com.leyou.search.repository.GoodsRepository;
import com.leyou.search.service.SearchService;
import com.leyou.starter.elastic.dto.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.nested.Nested;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.util.*;
import java.util.stream.Collectors;
import static com.leyou.search.constants.SearchSource.INDEX_SOURCE;
import static com.leyou.search.constants.SearchSource.SEARCH_INDEX;
import static com.leyou.search.constants.SearchSource.SUGGESTION_INDEX;


@Service
@Slf4j
public class SearchServiceImpl implements SearchService {


    @Autowired
    private GoodsRepository repository;

    @Autowired
    private ItemClient itemClient;

    @Autowired
    private RestHighLevelClient client;

    /**
     * 创建索引库和mapping映射关系
     */
    @Override
    public void createIndexAndMapping() {
        //1.删除已经存在的索引库
        try {
            repository.deleteIndex();
        } catch (Exception e) {
            log.info("索引库不存在,需要重新创建");
        }

        //2.根据条件创建索引库
        repository.createIndex(INDEX_SOURCE);
        log.info("索引库创建完毕");
    }

    /**
     * 加载数据
     */
    @Override
    public void lodaData() {
        log.info("开始插入文档数据");
        int page = 1;
        int rows = 100;
        while (true) {
            log.info("开始插入第{}页数据", page);
            //查询spu数据
            PageDTO<SpuDTO> pageInfo = itemClient.querySpuByPage(null, null, null, page, rows, true);
            //获取分页的详细信息
            List<SpuDTO> spuDTOList = pageInfo.getItems();

            //stream流获取goods集合
            List<Goods> goodsList = spuDTOList.stream().map(this::buildGoods).collect(Collectors.toList());
            //把数据存储到数据库
            repository.saveAll(goodsList);
            log.info("第{}页文档插入成功", page);
            //每存一页,就开始加载下一页
            page++;
            //文档存储到数据库完成,结束循环
            if (page > pageInfo.getTotalPage()) {
                log.info("第{}页文档插入成功,共插入{}份文档", page - 1, pageInfo.getTotal());
                break;
            }
        }


    }

    @Override
    public Mono<List<String>> getSuggestion(String key) {

        return repository.suggestBySingleField(SUGGESTION_INDEX, key);
    }

    @Override
    public Mono<PageInfo<Goods>> searchGoods(SearchParamDTO paramDTO) {
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        //1.关键字搜索
        MatchQueryBuilder basicQuery = buildBasicQuery(paramDTO);
        sourceBuilder.query(basicQuery);
        //2.排序
        String sortBy = paramDTO.getSortBy();
        Boolean desc = paramDTO.getDesc();
        if (StringUtils.isNotBlank(sortBy)) {
            sourceBuilder.sort(sortBy, desc ? SortOrder.DESC : SortOrder.ASC);
        }

        //3.分页
        int size = paramDTO.getSize();
        sourceBuilder.from(paramDTO.getPage()).size(size);

        //4.高亮
        sourceBuilder.highlighter(new HighlightBuilder().field(SEARCH_INDEX).preTags("<am>").postTags("</am"));

        //5.source过滤
        sourceBuilder.fetchSource(new String[]{"id", "image", "title", "sold", "prices"}, null);

        //4.搜索
        return repository.queryBySourceBuilderForPageHighlight(sourceBuilder);

    }

    private MatchQueryBuilder buildBasicQuery(SearchParamDTO paramDTO) {
        String key = paramDTO.getKey();
        if (StringUtils.isEmpty(key)) {
            throw new LyException(500, "搜索关键词不能为空");

        }
        return QueryBuilders.matchQuery(SEARCH_INDEX, key);
    }

    @Override
    public Mono<Map<String, List<?>>> searchFilter(SearchParamDTO paramDTO) {
        //1.条件
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        //1.1 query
        MatchQueryBuilder basicQuery = buildBasicQuery(paramDTO);
        sourceBuilder.query(basicQuery).size(0);
        //1.2 聚合分类
        sourceBuilder.aggregation(AggregationBuilders.terms("categoryAgg").field("categoryId").size(20));
        //1.3 聚合品牌
        sourceBuilder.aggregation(AggregationBuilders.terms("brandAgg").field("brandId").size(20));
        //1.4 聚合规格参数
        sourceBuilder.aggregation(AggregationBuilders.nested("specAgg", "specs")
                .subAggregation(AggregationBuilders.terms("specNameAgg").field("specs.name").size(20)
                        .subAggregation(AggregationBuilders.terms("specValueAgg").field("specs.value").size(20))));
        //2.创建请求
        SearchRequest request = new SearchRequest();

        //3.准备请求参数
        request.source(sourceBuilder);
        //4.调用client发起请求
        return  Mono.create(monoSink -> {
            client.searchAsync(request, RequestOptions.DEFAULT, new ActionListener<SearchResponse>() {
                @Override
                public void onResponse(SearchResponse searchResponse) {
                    //5.解析结果
                    Map<String, List<?>> filterList = handleAggregationResult(searchResponse);
                    //6.发射结果
                    monoSink.success(filterList);
                }

                @Override
                public void onFailure(Exception e) {
                    monoSink.error(e);
                }
            });
        });
    }

    @Override
    public void deleteSpuById(Long spuId) {
        repository.deleteById(spuId);
    }

    @Override
    public void saveSpuById(Long spuId) {
        //根据id查询spu
        SpuDTO spu = itemClient.queryGoodsById(spuId);
        //根据spu获取goods信息
        Goods goods = buildGoods(spu);
        //保存到索引库
        repository.save(goods);
    }

    private Map<String, List<?>> handleAggregationResult(SearchResponse response) {
        //5.解析结果
        Map<String, List<?>> filterList = new LinkedHashMap<>();
        //  获取聚合参数
        Aggregations aggregations = response.getAggregations();
        // 5.1 根据聚合名称category获取聚合
        Terms categoryAgg = aggregations.get("categoryAgg");
        //获取buckets
        List<? extends Terms.Bucket> buckets = categoryAgg.getBuckets();
        //取出buckets中的id
        List<Long> categoryIds = buckets.stream()
                .map(Terms.Bucket::getKeyAsNumber)
                .map(Number::longValue)
                .collect(Collectors.toList());

        //根据id查询 结果
        List<CategoryDTO> categories = itemClient.queryCategoryByIds(categoryIds);
        //封装到map中
        filterList.put("分类", categories);

        // 5.2  根据聚合名称Brand获取聚合
        Terms brandAgg = aggregations.get("brandAgg");
        //获取buckets
        List<? extends Terms.Bucket> brandBuckets = categoryAgg.getBuckets();
        //取出buckets中的id
        List<Long> brandIds = brandBuckets.stream()
                .map(Terms.Bucket::getKeyAsNumber)
                .map(Number::longValue)
                .collect(Collectors.toList());

        //根据id查询 结果
        List<BrandDTO> brands = itemClient.queryBrandByIds(brandIds);
        //封装到map中
        filterList.put("品牌", brands);
        //5.3 解析聚合参数
        //根据名称获取聚合结果
        Nested specAgg = aggregations.get("specAgg");
        //获取名称聚合
        Terms specNameAgg = specAgg.getAggregations().get("specNameAgg");
        //获取buckets
        for (Terms.Bucket nameBucket : specNameAgg.getBuckets()) {
            //根据bucket获取specName，就是规格参数的名称
            String specName = nameBucket.getKeyAsString();
            //根据名称获取聚合参数
            Terms specValueAgg = nameBucket.getAggregations().get("specValueAgg");
            //获取value
            List<String> valueList = specValueAgg.getBuckets()
                    .stream()
                    .map(Terms.Bucket::getKeyAsString)
                    .collect(Collectors.toList());
            filterList.put(specName,valueList);
        }
        return filterList;
    }

    private Goods buildGoods(SpuDTO spuDTO) {
        //1.商品标题, 可以拼接title,categoryName 和 brandName
        //String title = spuDTO.getTitle().concat(spuDTO.getCategoryName()).concat(spuDTO.getBrandName());
        StringBuffer stringBuffer = new StringBuffer().append(spuDTO.getTitle())
                .append(spuDTO.getCategoryName())
                .append(spuDTO.getBrandName());
        String title = stringBuffer.toString();

        //2.自动补全功能, 商品名称,品牌,分类名称
        Set<String> suggest = new HashSet<>();
        suggest.add(spuDTO.getName());
        suggest.add(spuDTO.getBrandName());
        String[] categoryNames = StringUtils.split(spuDTO.getCategoryName(), "/");
        CollectionUtils.addAll(suggest, categoryNames);
        //3.spu相关数据
        List<SkuDTO> skus = spuDTO.getSkus();
        Long spuId = spuDTO.getId();
        if (skus == null) {
            skus = itemClient.querySkuBySpuId(spuId);
        }
        //3.1 sold
        long sold = 0L;
        //3.2 price
        Set<Long> price = new TreeSet<>();
        for (SkuDTO skuDTO : skus) {
            price.add(skuDTO.getPrice());
            sold += skuDTO.getSold();
        }
        //3.3 image
        String image = StringUtils.substringBefore(skus.get(0).getImages(), ",");

        //4. 规格参数键值对
        ArrayList<Map<String, Object>> specs = new ArrayList<>();

        List<SpecParamDTO> params = itemClient.querySpecsValues(spuId, true);
        for (SpecParamDTO param : params) {
            Map<String, Object> map = new HashMap<>();
            map.put("name", param.getName());
            map.put("value", chooseSegment(param));
            specs.add(map);
        }

        //构建goods
        Goods goods = new Goods();
        goods.setUpdateTime(new Date());
        goods.setTitle(title);    //标题
        goods.setSuggestion(suggest); // 自动补全字段
        goods.setSpecs(specs);   // 规格参数键值对
        goods.setSold(sold);
        goods.setPrices(price); //  价格
        goods.setImage(image);  //  图片信息
        goods.setId(spuDTO.getId());
        goods.setCategoryId(spuDTO.getCid3());
        goods.setBrandId(spuDTO.getBrandId());


        return goods;
    }

    private Object chooseSegment(SpecParamDTO p) {
        Object value = p.getValue();
        if (value == null || StringUtils.isBlank(value.toString())) {
            return "其它";
        }
        if (!p.getNumeric() || StringUtils.isBlank(p.getSegments()) || value instanceof Collection) {
            return value;
        }
        double val = parseDouble(value.toString());
        String result = "其它";
        // 保存数值段
        for (String segment : p.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = parseDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if (segs.length == 2) {
                end = parseDouble(segs[1]);
            }
            // 判断是否在范围内
            if (val >= begin && val < end) {
                if (segs.length == 1) {
                    result = segs[0] + p.getUnit() + "以上";
                } else if (begin == 0) {
                    result = segs[1] + p.getUnit() + "以下";
                } else {
                    result = segment + p.getUnit();
                }
                break;
            }
        }
        return result;
    }

    private double parseDouble(String str) {
        try {
            return Double.parseDouble(str);
        } catch (Exception e) {
            return 0;
        }
    }

}
